package ipc

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"os"
	"sync"

	"code.byted.org/gopkg/logs"
	"github.com/pkg/errors"
	"go.uber.org/atomic"
)

const (
	ReadDataError IpcErrorCode = iota
	AcceptError
)

type IpcErrorCode int // nolint

type IpcHandler interface { // // nolint
	Handle(data []byte) string
	OnError(errorCode IpcErrorCode, err error)
}

type IpcServer struct { // // nolint
	lock     *sync.Mutex
	conns    *sync.Map
	connsMap *sync.Map
	connsNum *atomic.Uint64
	listener net.Listener
	address  string
	handler  IpcHandler
	started  *atomic.Bool
	accept   *sync.Once
}

func NewIPCServer(address string, handler IpcHandler) *IpcServer {
	return &IpcServer{
		lock:     new(sync.Mutex),
		conns:    new(sync.Map),
		connsMap: new(sync.Map),
		connsNum: new(atomic.Uint64),
		started:  new(atomic.Bool),
		handler:  handler,
		address:  address,
		accept:   new(sync.Once),
	}
}

func (i *IpcServer) SetHandler(handler IpcHandler) {
	i.handler = handler
}

func (i *IpcServer) StartServer() error {
	// check before locking
	if i.Started() {
		return errors.New("server has been started")
	}
	i.lock.Lock()
	defer i.lock.Unlock()
	// check after locking
	if i.Started() {
		return errors.New("server has been started")
	}

	var err error
	if err = os.RemoveAll(i.address); err != nil {
		return err
	}
	i.listener, err = net.Listen("unix", i.address)
	if err != nil {
		return err
	}
	i.started.Store(true)

	go func() {
		i.accept.Do(func() {
			logs.Info("ipc server start accept client")
			for {
				// stopServer has been called, cancel this goroutine
				if !i.Started() {
					break
				}
				conn, err := i.listener.Accept()
				if err != nil {
					logs.Error("ipc accept err:%s", err)
					if i.handler != nil {
						i.handler.OnError(AcceptError, err)
					}
					continue
				}
				logs.Info("accept a new client:%s, %v", conn.RemoteAddr().Network(), &conn)
				i.lock.Lock()
				i.addConn(&conn)
				i.lock.Unlock()
				go i.handleConn(&conn)
			}
		})
	}()
	return nil
}

func (i *IpcServer) handleConn(conn *net.Conn) {
	if conn == nil {
		logs.Errorf("handle message failed, connection is nil")
		return
	}
	var err error
	var data []byte
	for {
		if !i.Started() {
			logs.Error("ipc server stopped")
			goto end
		}
		_, data, err = ReadConn(conn)
		if err != nil {
			if i.handler != nil {
				i.handler.OnError(ReadDataError, err)
			}
			goto end
		}

		go func() {
			if i.handler != nil {
				appID := i.handler.Handle(data)
				if appID != "" {
					i.connsMap.LoadOrStore(appID, conn)
				}
			}
		}()
	}
	// processing of this connection is completed, delete it from conns
end:
	i.lock.Lock()
	i.deleteConn(conn)
	i.lock.Unlock()
}

func ReadConn(conn *net.Conn) (uint32, []byte, error) {
	if conn == nil {
		errMsg := "connection is nil"
		logs.Errorf("read from connection failed, %s", errMsg)
		return 0, nil, errors.New(errMsg)
	}
	sizeBuf := make([]byte, 4)
	_, err := io.ReadFull(*conn, sizeBuf)
	if err != nil {
		if err == io.EOF {
			return 0, nil, err
		}
		logs.Errorf("read from connection:%v failed, error:%v", conn, err)
		return 0, nil, err
	}

	size := binary.BigEndian.Uint32(sizeBuf)
	data := make([]byte, size)
	_, err = io.ReadFull(*conn, data)
	if err != nil {
		if err == io.EOF {
			return 0, nil, err
		}
		logs.Errorf("read from connection:%v failed, error:%v", conn, err)
		return 0, nil, err
	}
	return size, data, nil
}

func WriteConn(conn *net.Conn, data []byte) error {
	if conn == nil {
		errMsg := "conn is nil"
		logs.Errorf("write to connection failed, %s", errMsg)
		return errors.New(errMsg)
	}
	size := make([]byte, 4)
	binary.BigEndian.PutUint32(size, uint32(len(data)))

	_, err := (*conn).Write(size)
	if err != nil {
		logs.Errorf("write to connection:%v failed, data:%s, error:%s", conn, string(size), err)
		return err
	}

	_, err = (*conn).Write(data)
	if err != nil {
		logs.Errorf("write to connection:%v failed, data:%s, error:%s", conn, string(data), err)
		return err
	}
	return nil
}

func (i *IpcServer) addConn(conn *net.Conn) {
	if conn == nil {
		logs.Errorf("add connection failed, connection is nil")
		return
	}

	logs.Infof("connections before adding:%s", i.PrintConns())
	i.conns.Store(conn, nil)
	i.connsNum.Inc()
	logs.Infof("connections after adding:%s", i.PrintConns())
}

func (i *IpcServer) deleteConn(conn *net.Conn) {
	if conn == nil {
		logs.Errorf("delete connection failed, connection is nil")
		return
	}
	logs.Infof("connections before removing:%s", i.PrintConns())
	i.conns.Delete(conn)
	i.connsNum.Dec()
	if err := (*conn).Close(); err != nil {
		logs.Errorf("delete connection failed, close connection error:%v", err)
	}
	logs.Infof("connections after removing:%s", i.PrintConns())
}

func (i *IpcServer) PrintConns() string {
	num := i.connsNum.Load()
	res := fmt.Sprintf("number of connections:%d", num)
	if num == 0 {
		return res
	}
	res += ", contents: "
	i.conns.Range(func(k, v interface{}) bool {
		res += fmt.Sprintf("%v, ", k)
		return true
	})
	return res
}

func (i *IpcServer) SendBroadcastRawMessage(message []byte) error {
	if i.connsNum.Load() == 0 {
		err := errors.New("no connections")
		logs.Errorf("send message failed, error:%v", err)
		return err
	}
	i.lock.Lock()
	defer i.lock.Unlock()
	var err error
	// always return true to continue looping
	i.conns.Range(func(k, v interface{}) bool {
		err = WriteConn(k.(*net.Conn), message)
		return true
	})
	return err
}

func (i *IpcServer) SendRawMessage(appID string, message []byte) error {
	if i.connsNum.Load() == 0 {
		err := errors.New("no connections")
		logs.Errorf("send message failed, error:%v", err)
		return err
	}
	i.lock.Lock()
	defer i.lock.Unlock()
	v, ok := i.connsMap.Load(appID)
	if !ok {
		return errors.New("no app connection")
	}
	return WriteConn(v.(*net.Conn), message)
}

func (i *IpcServer) Started() bool {
	return i.started.Load()
}

func (i *IpcServer) Connected() bool {
	return i.connsNum.Load() > 0
}

func (i *IpcServer) StopServer() {
	i.lock.Lock()
	defer i.lock.Unlock()
	// set i.started to false before closing i.listener
	i.started.Store(false)
	_ = i.listener.Close()
	if i.address != "" {
		if err := os.RemoveAll(i.address); err != nil {
			logs.Error("remove address err:%s", err)
		}
	}
	logs.Infof("ipc server stopped")
}

func BuildSocketAddress(UUID string) string {
	return fmt.Sprintf("/tmp/whiteboard/%s.sock", UUID)
}
